Skip to content

Add configurable security context options for Postgres pods - #3224

Open
yyvess wants to merge 3 commits into
zalando:masterfrom
yyvess:spilo-security-context-options
Open

yyvess wants to merge 3 commits into
zalando:masterfrom
yyvess:spilo-security-context-options

Conversation

@yyvess

@yyvess yyvess commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Problem description

Postgres clusters cannot run in namespaces enforcing the Pod Security Standards restricted profile (Pod Security Admission, Kyverno, Gatekeeper, Azure Policy), even when Spilo already runs as a non-root user via spilo_runasuser. The profile requires fields the operator cannot emit:

Required by restricted Before this PR
pod runAsNonRoot: true no option
seccompProfile.type: RuntimeDefault (or Localhost) no option
container capabilities.drop: ["ALL"] no option (only additional_pod_capabilities to add)
container allowPrivilegeEscalation: false Spilo container only, sidecars / init containers / logical backup never get it

The usual workaround is a mutating webhook, which makes the generated StatefulSet differ from the live one and causes a rolling update with a switchover on every sync (#1879, #3210).

Changes

New operator options (ConfigMap and OperatorConfiguration, kubernetes section), independent of spilo_runasuser:

Option Effect Default
spilo_run_as_non_root runAsNonRoot in the pod security context, and in the container security contexts (see below) unset
spilo_seccomp_profile seccomp profile in the pod security context: RuntimeDefault, Unconfined or Localhost/<profile> (validated by the CRD pattern and the ConfigMap validation) unset
drop_pod_capabilities capabilities.drop of the containers, e.g. ALL empty
spilo_allow_privilege_escalation (existing) when false, now also applied to sidecars, init containers and the logical backup container true

How they are applied:

  • Pod level (runAsNonRoot, seccomp): applies to all containers of the Spilo and logical backup pods, including init containers; a container can still override it in its own security context.
  • Container level (dropped capabilities, allowPrivilegeEscalation: false, runAsNonRoot): filled into the Spilo, sidecar, init and logical backup containers only where the container does not define the field itself. Privileged containers are left untouched, and allowPrivilegeEscalation is not disabled for containers adding SYS_ADMIN (rejected by the API server).
  • runAsNonRoot is set on both levels on purpose: webhooks enforcing it (e.g. Kyverno, Rolling update loop on every resync when a mutating admission webhook injects security context fields (e.g. runAsNonRoot) #3210) inject it into the containers too, so with only the pod field the generated StatefulSet would keep differing from the mutated one and the operator would roll the cluster on every sync.
  • Containers and their security contexts are deep copied, so neither the cluster manifest nor the operator configuration is mutated.

spilo_run_as_non_root uses the name proposed in #3209. drop_pod_capabilities mirrors additional_pod_capabilities; happy to rename it to dropped_pod_capabilities as proposed in #2993 / #2244 if preferred.

All options are operator-wide, like spilo_privileged, spilo_allow_privilege_escalation and additional_pod_capabilities; only spiloRunAsUser / spiloRunAsGroup / spiloFSGroup exist per cluster. With spilo_run_as_non_root enabled, a cluster manifest overriding spiloRunAsUser with 0 fails to start its pods (documented). Per-cluster overrides of the security options (#3158, the spiloRunAsNonRoot field of #3209) are left to a follow-up covering all of them consistently.

Docs: parameter reference, a new "Pod Security Standards" section in administrator.md with a restricted recipe, chart values.yaml and sample manifests.

configuration:
  kubernetes:
    spilo_runasuser: 101
    spilo_runasgroup: 103   # primary group of postgres in Spilo, required for cron jobs
    spilo_fsgroup: 103
    spilo_run_as_non_root: true
    spilo_allow_privilege_escalation: false
    spilo_seccomp_profile: RuntimeDefault
    drop_pod_capabilities:
    - ALL

Upgrade impact

All new options are unset by default, so upgrading the operator does not change any pod template. Exception: setups that already set spilo_allow_privilege_escalation: false get allowPrivilegeEscalation: false on their sidecars and init containers too, which triggers one rolling update.

Cron, backups and certificate rotation without privilege escalation

The docs claimed spilo_allow_privilege_escalation is required by cron (#1730). This only holds when Spilo runs as root: as non-root, Spilo starts cron with LD_PRELOAD=cron_unprivileged.so (no-op seteuid/initgroups), so no setuid is needed. Verified with ghcr.io/zalando/spilo-18:4.1-p2 running as 101:103 with no-new-privileges, all capabilities dropped (CapBnd: 0) and the default seccomp profile: crontab -u postgres - installs the crontab and cron executes the jobs. Only Spilo's optional renice job is skipped (Spilo logs it). Note that spilo_runasgroup must be 103: with the crontab group (101) cron silently fails to run jobs. The spilo_allow_privilege_escalation docs were updated accordingly.

Linked issues

Fixes #2081 — runAsNonRoot on the Postgres StatefulSet
Fixes #2291 — allowPrivilegeEscalation: false for the exporter sidecar (with spilo_allow_privilege_escalation: false)
Fixes #3210 — rolling update loop when a webhook injects runAsNonRoot (configure the same value with spilo_run_as_non_root; webhooks injecting fields the operator has no option for, e.g. sysctls #3095, still diverge)

Addressed for Spilo and logical backup pods:

Partially addressed:

Not covered: connection pooler (#1722, see #3105), operator UI (#2682), per-cluster overrides in the postgresql manifest.

Related pull requests

Checklist

  • Unit tests: defaults leave the pod template unchanged; all options on Spilo, sidecar, init and logical backup containers; runAsNonRoot on pod and container level; container-defined fields win; privileged and SYS_ADMIN containers; cluster manifest and operator configuration not mutated; ConfigMap parsing and seccomp validation (invalid values fail validation and StatefulSet generation)
  • CRD and deepcopy regenerated
  • Docs, chart values and sample manifests updated

🤖 Generated with Claude Code

Running Postgres clusters under the restricted Pod Security Standard
requires settings the operator could not configure. Add operator options
that are independent of the configured user ID:

- spilo_runasnonroot: sets runAsNonRoot in the pod security context
- spilo_seccomp_profile: sets the seccomp profile in the pod security
  context (RuntimeDefault, Unconfined or Localhost/<profile>)
- drop_pod_capabilities: capabilities dropped from the containers

Pod level settings apply to all containers. Dropped capabilities and a
disabled spilo_allow_privilege_escalation are filled into the Spilo,
sidecar, init and logical backup containers, unless a container defines
them itself or runs privileged. The cluster manifest and operator
configuration are not modified.

The new options are unset by default, so upgrading the operator does not
change pod templates, except for setups that already disable
spilo_allow_privilege_escalation: their sidecars and init containers now
get allowPrivilegeEscalation: false as well.
Replace the validation regexp and the pod template conversion with one
config.SeccompProfile function used by both, so the accepted values are
defined in one place and an invalid value can no longer be turned into an
arbitrary profile type. Generating the pod template fails on an invalid
value instead.
Align the option name with the one proposed in zalando#3209.

runAsNonRoot is now also filled into the Spilo, sidecar, init and logical
backup containers, unless they define it themselves or run privileged.
Admission webhooks enforcing runAsNonRoot (e.g. Kyverno) inject it on
container level too; with only the pod field set, the generated
StatefulSet kept differing from the mutated one and the operator rolled
the cluster on every sync (zalando#3210).
@FxKu FxKu added this to the wishlist milestone Nov 10, 2026
@FxKu FxKu added the major Major feature changes or updates, e.g. feature rollout to a new country, new API calls. label Nov 10, 2026
@FxKu FxKu moved this to Waiting for review in Postgres Operator Nov 10, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

major Major feature changes or updates, e.g. feature rollout to a new country, new API calls.

Projects

Status: Waiting for review

3 participants